<?php

/*
 * Title: atlast.class.inc.php
 * Provides an API between FoxyBack and AtLastFulfillment.
 *
 */

include_once('config.inc.php');
include_once('foxyback.class.inc.php');

class AtLast {

	// set class variables
	var $error_info = '';

	var $ship_methods = array();

	// Initialize the class
	function __construct() {
		// Prepare the list of valid ship method codes for AtLast.
		// Map FoxyCart shipping codes to AtLast codes.
		$this->ship_method_codes = array(
			// USPS
			'USPS First Class' 						=> 'POM1C', 
			'USPS: First Class Mail'				=> 'POM1C', 
			'USPS Media Mail'						=> 'POM4B',
			'USPS: Media Mail'						=> 'POM4B',
			'USPS Priority'							=> 'POM1M',
			'USPS: Priority Mail'					=> 'POM1M',
			'USPS Express'							=> 'POMEX',
			'USPS: Express Mail'					=> 'POMEX',
			'USPS International'					=> 'POMIM',
			'USPS: First-Class Mail International'	=> 'POMIM', // ?
			'USPS Global Priority'					=> 'POMGP',
			'USPS: Priority Mail International'		=> 'POMGP',
			'USPS Global Express'					=> 'POMGE',

			// UPS
			'UPS Ground'							=> 'UPSG',
			'UPS: Ground'							=> 'UPSG',
			'UPS Ground Residential'				=> 'UPSGR',
			'UPS 3 Day Select'						=> 'UPS3D',
			'UPS: 3 Day Select'						=> 'UPS3D',
			'UPS 2nd Day'							=> 'UPS2D',
			'UPS: 2nd Day Air'						=> 'UPS2D',
			'UPS Next Day Air'						=> 'UPS1D',
			'UPS: Next Day Air'						=> 'UPS1D',
			'UPS Next Day Air Saver'				=> 'UPS1S',
			'UPS Next Day Early AM'					=> 'UPS1E',
			'UPS: Next Day Air Early AM'			=> 'UPS1E',
			'UPS Next Day Saturday Delivery'		=> 'UPSSD',
			'UPS Worldwide Express'					=> 'UPSI',
			'UPS: Worldwide Express'				=> 'UPSI',
			'UPS Worldwide Expedited'				=> 'UPSWE',
			'UPS: Worldwide Expedited'				=> 'UPSWE',

			// FedEx
			'FedEx Home Delivery'					=> 'FEXHD',
			'FedEx: Home Delivery'					=> 'FEXHD',
			'FedEx Ground'							=> 'FEXG',
			'FedEx: Ground'							=> 'FEXG',
			'FedEx Express Saver'					=> 'FEXP',
			'FedEx: Express Saver'					=> 'FEXP',
			'FedEx 2 Day'							=> 'FEXEB',
			'FedEx: 2 Day'							=> 'FEXEB',
			'FedEx Standard Overnight'				=> 'FEXSO',
			'FedEx: Standard Overnight'				=> 'FEXSO',
			'FedEx Priority Overnight'				=> 'FEXPO',
			'FedEx: Priority Overnight'				=> 'FEXPO',
			'FedEx First Overnight'					=> 'FEXFB',
			'FedEx: First Overnight'				=> 'FEXFB',
			'FedEx International Economy'			=> 'FEXIE',
			'FedEx: International Economy'			=> 'FEXIE',
			'FedEx International Priority'			=> 'FEXIC',
			'FedEx: International Priority'			=> 'FEXIC',

			// Other
			'Best Rate Ground Residential'			=> 'BRRE',
			'Best Rate Ground Commercial'			=> 'BRCM',
			'Best Rate Overnight'					=> 'Best NDAY',
		);
	}

	// Create a shipment for the specified transaction.
	function createShipment($transaction_id) {
		global $modx, $ATLAST_CONFIG;

		if (!$transaction_id) {
			$this->error_info = 'Missing required parameter (transaction_id)';
			return NULL;
		}

		// Get the configuration options.
		if (!$ATLAST_CONFIG['client_id']) {
			$this->error_info = 'No client ID defined in the config file!!!';
			return NULL;
		}

		$foxyback = new FoxyBack();

		// Get all of the information about this transaction - not just a simple "view".
		$filter = "|transaction_id,$transaction_id,2";
		$transaction_array = $foxyback->getTransactionList(array('filter' => $filter));
		$transaction = $transaction_array['LIST'][0];

		// Prepare the fields to post to AtLast.
		$fields = array(
			'ClientID'			=> $ATLAST_CONFIG['client_id'],
			'OrderID'			=> $transaction_id,
			'OrderDate'			=> $transaction['transaction_date'],
			'FirstName'			=> $transaction['first_name'],
			'LastName'			=> $transaction['last_name'],
			'Company'			=> $transaction['company'],
			'Address1'			=> $transaction['address1'],
			'Address2'			=> $transaction['address2'],
			'City'				=> $transaction['city'],
			'State'				=> $transaction['state'],
			'Zip'				=> $transaction['postal_code'],
			'Country'			=> $transaction['country'],
			'Email'				=> $transaction['email'],
			'Phone'				=> $transaction['phone'],
		);

		// Get information for the packing slip from custom fields on this transaction.
		if (array_key_exists('custom_fields', $transaction) 
			&& is_array($transaction['custom_fields']))
		{
			foreach ($transaction['custom_fields'] as $custom_field) {
				if ($custom_field['field_name'] == 'infoline') {
					$fields['InfoLine'] = $custom_field['field_value'];
				}
				elseif ($custom_field['field_name'] == 'reference1') {
					$fields['Reference1'] = $custom_field['field_value'];
				}
			}
		}

		// Build several fields for the products.
		$ship_method = '';
		$sku_string = '';
		$quantity_string = '';

		if (!array_key_exists('shipto_addresses', $transaction)) {
			$this->error_info = 'No shipto addresses defined on this transaction.';
			return NULL;
		}

		// TODO: handle multiple shipto addresses properly:
		// 'shipto' on the detail matches with 'address_name' on the shipto.

		// Loop through the shipto addresses.
		foreach ($transaction['shipto_addresses'] as $address) {
			// Get the ship method to use.
			$service_description = $address['service_description'];
			if (array_key_exists($service_description, $this->ship_method_codes)) {
				$ship_method = $this->ship_method_codes[$service_description];
			}
			else {
				// This will be put on hold with AtLast since it won't be recognized.
				$ship_method = $service_description;
			}

			// Update the address if defined on this entry.
			if ( $address['first_name'] != '' && $address['last_name'] != '' 
				&& $address['address1'] != '' 
			) {
				$fields['FirstName'] = $address['first_name'];
				$fields['LastName'] = $address['last_name'];
				$fields['Address1'] = $address['address1'];
				$fields['Address2'] = $address['address2'];
				$fields['City'] = $address['city'];
				$fields['State'] = $address['state'];
				$fields['Zip'] = $address['postal_code'];
				$fields['Country'] = $address['country'];
				$fields['Phone'] = $address['phone'];
			}

			// Just use the first one for now.
			break;
		}

		// Set the ship method.
		$fields['ShipMethod'] = $ship_method;

		// Process the individual products.
		foreach ($transaction['transaction_details'] as $detail) {
			// There will be serious problems if the product_code is empty here.....
			if ($detail['product_code'] == '') {
				//$this->error_info = "The product code is EMPTY for transaction $transaction_id and detail ".$detail['transaction_detail_id'];
				//return NULL;
				// Use the product name so it isn't blank.
				$detail['product_code'] = $detail['product_name'];
			}
			
			$sku_string .= $detail['product_code'] . ',';
			$quantity_string .= $detail['product_quantity'] . ',';
		}

		// Set the SKU and quantity.
		$fields['SKU'] = $sku_string;
		$fields['Qty'] = $quantity_string;

		// Include extra fields for international orders.
		// TODO: determine if this is sufficient.
		if ($fields['Country'] != 'US') {
			$fields['DeclaredValue'] = '150.00';
			$fields['Contents'] = 'Cell phone accessory';
		}

		/*
		/////////////////////////////////////////////////////////////////////////////////////////////////
		// THIS IS DISABLED FOR DEBUGGING
		$report = "The following information was posted to AtLast: \n".print_r($fields, true);
		//$this->error_info = $report;
		// Send a debugging email.
		$notify = new Notify();
		$notify->sendErrorEmail($report);
		return 1;
		/////////////////////////////////////////////////////////////////////////////////////////////////
		 */

		// Other fields that may be added in the future:
		// Fax, GiftBag, GiftBagColor, GiftMessage, Reference2

		// Post the form.
		$post_url = 'http://www.atlastfulfillment.com/api/postShipment.asp';
		// Set the GET string, since POST doesn't seem to be working
		$get_url = $post_url . '?';
		$counter = 0;
		foreach ($fields as $name => $value) {
			$get_url .= ($counter == 0) ? '' : '&';
			$get_url .= rawurlencode($name) . '=' . rawurlencode($value);
			$counter ++;
		}

		$notify_obj = new Notify();
		$report = "The following information was posted to AtLast: \n".print_r($fields, true) . "\n\nGET URL: " . $get_url;

		$ch = curl_init();
		// curl_setopt($ch, CURLOPT_URL, $post_url);
		curl_setopt($ch, CURLOPT_URL, $get_url);
		curl_setopt($ch, CURLOPT_TIMEOUT, 30);
		// curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
		curl_setopt($ch, CURLOPT_HTTPGET, true);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		$response = curl_exec($ch);
		curl_close($ch);

		// Check the return status.
		if ($response == 'MISSINGCLIENTID') {
			$this->error_info = 'AtLast POST error: the ClientID was missing.';
			$report .= $this->error_info;
			$notify_obj->sendErrorEmail($report);
			return NULL;
		}
		elseif ($response == 0) {
			$this->error_info = 'AtLast POST error: one or more fields had invalid data.';
			$report .= $this->error_info;
			$notify_obj->sendErrorEmail($report);
			return NULL;
		}
		else {
			// Success!
			$report .= "Success.";
			// Note: only send a confirmation message when debugging.
			$notify_obj->sendMessageEmail($report);

			// This is the Shipment ID. Save it to the transaction.
			$edit_spec = array(
				'ship_reference_number' => $response,
				'notes' => $transaction['notes'] . "\nTransaction sent to AtLastFulfillment.",
			);

			$edit_id = $foxyback->editTransaction($transaction_id, $edit_spec);
			if (!$edit_id) {
				$this->error_info = 'Error while editing the transaction after a successful post to AtLast.';
				return NULL;
			}
		}

		return 1;
	}
	// end of createShipment
	
	// The following function will be called by a cron job.
	// Check for any fulfilled shipments with AtLast.
	function receiveFulfilledShipments() {
		global $ATLAST_CONFIG;

		// Output debugging info.
		$debug = false;

		if (!$ATLAST_CONFIG || !is_array($ATLAST_CONFIG) || !$ATLAST_CONFIG['ftp_server'] 
			|| !$ATLAST_CONFIG['ftp_username'] || !$ATLAST_CONFIG['ftp_password'] 
			|| !$ATLAST_CONFIG['xml_filename'] || !$ATLAST_CONFIG['download_path'] 
		) {
			$this->error_info = 'Missing FTP info in the config file!!!';
			$this->notifyError($debug);
			return NULL;
		}

		// Make sure that the download directory is writable.
		if (!is_writable($ATLAST_CONFIG['download_path'])) {
			$this->error_info = 'The download directory does not exist or is not writable: '.$ATLAST_CONFIG['download_path'];
			$this->notifyError($debug);
			return NULL;
		}
		if ($debug) print "connecting...";

		// Set up a basic FTP connection
		$connection_id = ftp_connect($ATLAST_CONFIG['ftp_server']); 

		// login with username and password
		$login_result = ftp_login($connection_id, $ATLAST_CONFIG['ftp_username'], $ATLAST_CONFIG['ftp_password']); 
		if ((!$connection_id) || (!$login_result)) { 
			$this->error_info = 'FTP connection failed';
			$this->notifyError($debug);
			ftp_close($connection_id); 
			return NULL;
		}
		if ($debug) print "done\n";

		// Turn on passive mode.
		if (!$pasv = ftp_pasv($connection_id, true)) {
			$this->error_info = 'Could not enable passive mode';
			$this->notifyError($debug);
			ftp_close($connection_id); 
			return NULL;
		}

		$ftp_path = $ATLAST_CONFIG['ftp_path'];
		if ($debug) {
			if (!$filelist = ftp_nlist($connection_id, $ftp_path)) {
				$this->error_info = 'Couldn\'t list contents';
				$this->notifyError($debug);
				ftp_close($connection_id); 
				return NULL;
			}
			if ($debug) print_r($filelist);
		}

		// Determine where the file will be saved on the local filesystem.
		// Do not extract the xml_filename here since it must be done in the loop below.
		$download_path = $ATLAST_CONFIG['download_path'];

		if ($download_path != '' && !preg_match("/\/$/", $download_path)) {
			// Add a trailing slash.
			$download_path .= '/';
		}

		// The dates that a file was successfully received/processed are stored inside a log file.
		$last_time = '';
		$log_name = $download_path.'atlast_log.txt';

		// Open it to check for the last success.
		if (file_exists($log_name)) {
			// Read the file into an array to get the last line.
			$log_array = file($log_name);
			$last_line = array_pop($log_array);
			preg_match("/^([^\s]+)/", $last_line, $matches);
			$last_time = strtotime($matches[1]);
			if ($debug) print "Got last time from log: $last_time\n";
		}
		else {
			// File does not exist, so create it.
			if (!touch($log_name)) {
				$this->error_info = "Couldn't create the log file.\n";
				$this->notifyError($debug);
				ftp_close($connection_id); 
				return NULL;
			}

			// Immediately change the permissions on the file to be WORLD-WRITABLE.
			// This is necessary because Plesk servers automatically change the ownership of files, 
			// so a file created now might not be writable by this same user on the next execution.
			if (!chmod($log_name, 0777)) {
				$this->error_info = "Couldn't set the permissions on the log file.\n";
				$this->notifyError($debug);
				ftp_close($connection_id); 
				return NULL;
			}
		}

		if ($last_time == '') {
			// Default to yesterday's date.
			$last_time = strtotime("-1 days");
		}
		else {
			// This is the last time we processed successfully, so advance to the NEXT day.
			$last_time += 86400;
		}
		
		// Normalize to the beginning of the day to account for DST changes.
		$last_time = strtotime(date('Y-m-d', $last_time));

		// Stop on today.
		$stop_time = strtotime(date('Y-m-d'));

		$foxyback = new FoxyBack();
		$notify_obj = new Notify();

		// Record errors (fatal) and warnings (non fatal).
		$error_string = '';
		$warning_string = '';

		if ($debug) print "Process $last_time (relative to now: $stop_time)\n";

		// Check every date since the last success.
		while ($last_time < $stop_time) {
			$last_date = date('Y-m-d', $last_time);
			if ($debug) print "Look for file on $last_date ($last_time)\n";

			// Track the number of shipments processed for this day.
			$shipment_count = 0;

			$filename = $ATLAST_CONFIG['xml_filename'];

			// Should the target filename contain the current date?
			// It might contain a date format like [+date:yyyymmdd+]
			if (preg_match("/\[\+date:([-.ymd]+)\+\]/", $filename, $matches)) {
				$date_string = $matches[1];

				// Replace the date 'elements' in the date string with the actual formatted ones.
				$date_string = preg_replace("/yyyy/", date('Y', $last_time), $date_string);
				$date_string = preg_replace("/yy/", date('y', $last_time), $date_string);
				$date_string = preg_replace("/mm/", date('m', $last_time), $date_string);
				$date_string = preg_replace("/dd/", date('d', $last_time), $date_string);
				$date_string = preg_replace("/d/", date('j', $last_time), $date_string);

				// Insert the final date string into the filename.
				$filename = preg_replace("/\[\+date:[-.ymd]+\+\]/", $date_string, $filename);
			}

			if ($debug) print "downloading $filename to $download_path\n";

			// Check if this file exists on the ftp server.
			$file_size = ftp_size($connection_id, $ftp_path.$filename);
			if ($file_size > 0) {
				// Remove the local file if it exists already.
				if (file_exists($download_path.$filename)) {
					if (!unlink($download_path.$filename)) {
						// Not a serious error.
						$warning_string .= "Could not locally remove $filename prior to download\n";
					}
				}

				// Download the file
				$download = ftp_get($connection_id, $download_path.$filename, $ftp_path.$filename, FTP_ASCII); 
				if (!$download) {
					// Fatal error, so exit the loop.
					$error_string .= "FTP download of $filename failed\n";
					break;
				}
			}
			else {
				// File does not exist for this day. Check if it is on the local filesystem already.
				if (!file_exists($download_path.$filename)) {
					// Skip to the next day WITHOUT recording that today was successful.
					// This is not really an "error" since it will occur anytime AtLast has not posted files.
					if ($debug) print "File $filename does not exist either on FTP or locally.\n";
					$last_time += 86400;
					continue;
				}
			}

			// Open the file and parse it.
			if (!$raw_xml = file_get_contents($download_path.$filename)) {
				// Fatal error, so exit the loop.
				$error_string .= "Error reading file $filename\n";
				break;
			}
			$data = new SimpleXMLElement($raw_xml);

			// Since the download location is probably publicly accessible, change the permissions of the 
			// downloaded file to NOT be world-readable, especially since it contains sensitive customer data.
			if (!chmod($download_path.$filename, 0600)) {
				$this->error_info = "Couldn't set the permissions on the downloaded XML file.\n";
				$this->notifyError($debug);
				ftp_close($connection_id); 
				return NULL;
			}

			// Process each shipment in the file.
			foreach ($data as $shipment) {
				// Process a shipment. Cast each value as a string, otherwise they're objects.
				$transaction_id = (string) $shipment->OrderID;
				$ship_date = (string) $shipment->ShippedDate;
				$ship_method = (string) $shipment->ShipMethod;
				$tracking_number = (string) $shipment->Tracking;

				// The XML also contains some fields that we ignore currently:
				// Name, Postage, Items (a summary of the items, each containing: SKU, Lot)

				// Convert the ship date from (m/d/yyyy) to yyyy-mm-dd
				$ship_date = preg_replace("/^(\d+)\/(\d+)\/(\d+)$/", "$3-$1-$2", $ship_date);

				// Make sure the transaction is not processed twice.
				$transaction_view = $foxyback->getTransactionView($transaction_id);
				if (!is_array($transaction_view) || !array_key_exists('transaction_id', $transaction_view)) {
					// Unrecognized transaction.

					// Disable this warning message if AtLast is used to manage orders that are not 
					// inside FoxyBack.
					//$warning_string .= "Transaction $transaction_id from the XML does not exist in the order database!\n";
					continue;
				}

				if ($transaction_view['ship_tracking_number'] != '') {
					// This transaction has already been processed, so skip to the next one.
					if ($debug) print "Transaction $transaction_id processed already.\n";
					continue;
				}

				// Append to the notes.
				$notes = $transaction_view['notes'];
				if ($notes != '' ) {
					$notes .= "\n";
				}
				$notes .= "Order shipped on $ship_date via $ship_method";

				// Save the tracking number to this transaction in the db.
				$edit_spec = array(
					'ship_tracking_number' 	=> $tracking_number,
					'ship_date' 			=> $ship_date,
					'ship_method' 			=> $ship_method,
					'notes' 				=> $notes,
				);

				// Check if the order status is "fulfilled" currently.
				if ($transaction_view['order_status_id'] == $STATUS_ARRAY['Sent to AtLast']) {
					// Update the status to "shipped". This will trigger a status message to be sent to the customer.
					$edit_spec['order_status_id'] = $STATUS_ARRAY['Shipped'];
					$edit_spec['status_notes'] = 'Order fulfilled by AtLast - ready to ship.';
				}

				// The edit must occur BEFORE emailing the customer, since the email will not send if the
				// tracking number on the transaction is empty.
				if (!$edit_id = $foxyback->editTransaction($transaction_id, $edit_spec)) {
					$error_string .= "Error editing transaction $transaction_id: ".$foxyback->error_info."\n";
					continue;
				}

				if ($transaction_view['order_status_id'] == $STATUS_ARRAY['Shipped']) {
					// Order is already marked as "shipped". Email the customer with the tracking id.
					if (!$send_id = $notify_obj->sendStatusMessage($transaction_id, $transaction_view['order_status_id'])) {
						$error_string .= "Error in sendStatusMessage on transaction $transaction_id: ".$notify_obj->error_info."\n";
						continue;
					}
				}

				if ($transaction_view['order_status_id'] != $STATUS_ARRAY['Shipped'] 
					&& $transaction_view['order_status_id'] != $STATUS_ARRAY['Sent to AtLast']) 
				{
					// Order status is not appropriate for shipping. Send a warning message.
					// TODO: get the status name for the message.
					$message = "Warning: transaction $transaction_id currently has order status ".$transaction_view['order_status_id']." set, which is not appropriate for shipping, yet the order was just fulfilled and shipped by AtLast. Please review this order.\n";
					$notify_obj->sendErrorMessage($message);
				}

				// Shipment successfully processed.
				$shipment_count++;
			}
			// Done processing all shipments.

			// Only record a success if there were no errors for this day.
			if ($error_string == '') {
				if ($debug) print "Write $last_date to the log\n";
				// Write the last processed date to the file.
				// Be sure to append to the existing file so the history is not lost.
				if (!file_put_contents($log_name, "$last_date\t $shipment_count\n", FILE_APPEND)) {
					// Fatal error, so exit the loop.
					$error_string .= "Error writing to log file\n";
					break;
				}
			}

			// Increment to the next day.
			$last_time += 86400;
		}
		// Done processing all possible days.
		
		// Close the FTP connection.
		ftp_close($connection_id); 

		// Check for any errors or warnings.
		if ($error_string != '' || $warning_string != '') {
			// Some error occurred, so report it.
			if ($error_string != '') {
				$this->error_info = "Errors generated in AtLast:receiveFulfilledShipments!\n$error_string\n";
			}
			if ($warning_string != '') {
				$this->error_info .= "Warnings generated in AtLast:receiveFulfilledShipments!\n$warning_string\n";
			}
			$this->notifyError($debug);
			return NULL;
		}

		return 1;
	}
	// end of receiveFulfilledShipments

	// Send an error email with the contents of the current error string.
	function notifyError($print_error=false) {
		if ($print_error) {
			print "ERROR! ".$this->error_info."\n";
			return 1;
		}

		$notify_obj = new Notify();
		$notify_obj->sendErrorEmail($this->error_info);
		return 1;
	}
}
// end of class

?>
